ECE5725: Arcade Console with a Vending Machine Full of Prizes!


By Sarah Ellenbogen (sje57), Zack Nelson (zen3), and Matt Hales (mah426)


Demonstration Video


Introduction

    For our project we decided to create an arcade game machine and corresponding prize dispenser. The system consists of two parts each containing their own Raspberry Pi for control. The game portion includes a Raspberry Pi 4 with a 7 inch monitor, which is running the MAME gaming emulator so we can play classic arcade games. The python program reads the memory of the emulator so we could include a points awarding system. The gaming system also includes buttons and a joystick for user control.

    The prize dispenser is set up as a vending machine using a Raspberry Pi Zero W. The Pi controls motors to rotate coils and dispense prizes as well as runs controls with an associated GUI to allow users to select and pay for prizes based on the amount of tickets they have earned.

    The interface between the prize dispenser and the gaming console are RFID cards and readers/writers. By scanning the card on the RFID reader/writer on the console side, the amount of points awarded will be recorded on the card and by scanning the card on the prize dispenser side the user can pay for prizes and subsequently have tickets deducted from their card.


Generic placeholder image

Project Objective:

   We designed and constructed a simple arcade emulator and prize vending machine. We wanted to use preexisting games one would normally find in an arcade and create a simple program that would be able to read the memory files of each game to generate tickets based on the number of points the player scores. Players could then scan their RFID cards to store their tickets and redeem prizes in the vending machine based on the number of tickets they have.


Design and Testing

RFID

    We received RFIDs and cards from Adafruit and decided that the simplest way forward would be to follow their online tutorial. They suggested using SPI as the communication protocol as it is the most reliable on the Raspberry Pi. The libraries that are written for the RFID readers/writers for systems that use python utilize circuitPython libraries including busio, for the communication protocols, and digitalio, for the GPIO pins. To use the RFID library we had to download circuitPython on the Raspberry Pis, which was done by simply following another Adafruit tutorial. Once we downloaded the Circuitpython libraries we checked to see if our communication buses were working properly. We noticed that no SPI channels were listed, which upon further inspection was because the PiTFT was using this channel. By simply enabling the second SPI channel we were able to get the RFID readers configured. We incrementally tested the readers/writers by running their example code for reading and writing, which wrote hex values to the blocks of the card. This checked proper wiring and the next task was to get the devices integrated into our main code. We quickly realized that we could not simply write integers to blocks of the cards because each block consists of a 16 position byte array. We found functions that convert integers to and from byte arrays, which made writing and reading ticket values very simple. Through testing we also noticed that the cards could not be read from or written to without authenticating the block of data. We simply used the factory set key to authenticate each of the blocks, which was included in the library examples.

Gaming Console

    For the gaming console, we initially started with using the common emulator software known as RetroPie. This operating system has many emulators prebuilt in the OS, making it easy for anyone to add the ROM files of their favorite games and play. One of these emulators RetroPie includes is MAME. We chose MAME over the other emulators as MAME has the ability to run a debugger, which gives the user access to the game's memory as well as somewhat strong documentation. However, the MAME version built into RetroPie is hightly limitated including the lack of a debugger and command line inputs for a script to interface with. As a result, reading the memory would not be easy through RetroPie, so we needed another solution.

    The best documented alternative was to install the full Raspberry Pi OS, with the ability to have a desktop. Having the desktop is important because it includes the open GL drivers that MAME uses for high speed rendering with the Pi's GPU (in contrast pygame does all the rendering on the CPU and is therefore much less responsive). Raspberry Pi OS allows for the full version of the MAME emulator which includes the needed debugger and command line. The debugger allowed us to view the current memory and search for addresses that change in the way we'd expect to corespond to the player lives and score. These two values were essential for our program to determine when the player finished the game (when to dispense tickets) and to read the player’s score (how many tickets to dispense). We also added controls, including buttons and a joystick, which are connected through a USB joystick controller. This allowed for a much simpler integration process and higher reliability because the USB joystick is just a basic HID joystick meaning it has full suport built into Rasbian. We just needed to map the buttons and joystick directions (eg. the joystick is mounted upsidown internaly) to the ones that we needed within the emulator. For video output this is provided through a Raspbery Pi Touch Display which connects with a ribbon cable to the Pi's DSI port. Being the oficial Raspberry Pi display it works just plug and play with all the drivers built into the default Rasbian image just as HDMI monitors do.

    MAME has the option to be booted with a console which is their own scripting enviornment through the command line. This specifically uses an incomplete implementation of the Lua scripting language with only partial documentation. To read the RAM of the current game through our python script we first boot up MAME using Python's subprocess library which allows taking over controll of the stdin and stdout for the subproces. This makes it possible to send LUA script commands with the write function and capture their output by flushing the contents of stdin. The function reads the values of memory using the run function that I wrote. This will write the requested command to stdin and then flush it. The result is then read from the stdout using read1() within the display function. If a response is expected the first line will then be parsed and its result will be returned as an int.

    When players die there is a short sequence requiered to give them their tickets. First of all the emulater is paused using: run("emu.pause()", False) which puts emu.pause() in the command line and doesn't expect a response. Next the users score has to be found. Games at the time generally had an odd way of storing the score where each decimal digit is stored in its own nibble (half byte) or byte depending the on the memory avalability. This requiered a complicated system to calulate the users score, for example in PacMan the ones and tens value would be retreved with: run("print(mem:read_u8(0xee80))", True). This would then follow for each pair of digits with the consecutive memory addresses. We then use the score to determine the number of tickets the user should get. We decided that in Galaga and Space invaders every point is equal to one ticket, while in PacMan, every ten points is equal to one ticket (meaning 5750 points would be 575 tickets). The user will then be prompted to tap their RFID card and collect their tickets. The prompt is created using the lua popmessage function. Python string concatentation withen the function itself allows for variables to be displayed to the user. The function is: run("manager.machine:popmessage('tap card to collect your " + str(score) + " tickets')", False) followed by: run("manager.machine:popmessage('you had " + str(cardpoints) + " tickets you now have " + str(newpoints) + " tickets!')", False) to show sucess. Through play testing we noticed that in the event that the player scans their card too quickly, the reader/writer does not have sufficient time to read and write the code will as it attempts to convert a none type into a byte array. This raises an exception which we are able to catch and request the user to scan again with another pop up message through. This ensures that the player scans their card long enough for the system to read the current amount of tickets on the card, add the newly awarded tickets, and write them back to the RFID card. Now that the tickets have been safely stored on the player’s RFID card, we can issue run("emu.unpause()", False) and let the player continue with their game (this command was not in any of the documentation and could only be found within comments in the source code).

    This framework makes it simple for users to add games to the arcade system. Their first step is just to add the compadable rom of their choice and add its name to the check. The second is to identify the memory address for the player’s current number of lives. This was done through the debugger and using their cheat commands. By issuing cheatnext decrease,1 in the debugger after the player loses a life, we were able to identify which memory addresses had decreased by one. With each life lost, this list would decrease and show only one memory address. We wanted to be sure that this was the memory address associated with lives, so we would specifically search for that address in the debugger window while playing the game normally and if the value decreased by 1 with each life lost, we knew this was the player’s number of lives. Lastly the location where score's are saved in memory needs to be identified as well as the unique way it needs to be parced. This process can be confusing because the memory is usually little endian. Little endian is a method where the lowest values are placed at the first index. This can make finding the score confusing as a score of 7450 will be displayed across two different addresses, with the address holding score 50 being a lower address than the address holding 74. Essentially, the user will see 5074 in the memory. With this in mind, it became easier for us to identify the specific memory addresses by following this pattern. If either of the addresses can't be found there is also some sparse documentation that can be found in forms for most games which can be a big help.

    Emulator Overseer Steps

Internal Console Wiring

Vending Machine

    Since we were using two separate Raspberry Pis for control in our overall system and the Pi controlling the vending machine was a Pi Zero instead of a 4 we had to configure the system from scratch. Instead of starting from the most recent Bullseye operating system as we did in lab 1 we decided it might be best to start from Buster as recommended on the PiTFT startup guide on the Adafruit website. From there we were able to run their premade TFT installation script to get visuals displayed on the screen. To get touch enabled we had to perform the wheezy downgrade as we did in Lab 2, and we decided to use the bash script that Adafruit had described in the previously mentioned tutorial. The one issue that we ran into was that the provided bash script assumes that Buster is the most recent stable release as it was not written recently. It took us some time to figure out that was the reason it was not running. We had to specify that the current OS was Buster instead of using the keyword “stable”.

    When setting up the PiTFT we decided that we wanted the screen to be rotated 0 degrees (portrait orientation) to better accommodate the vending machine keypad. After performing this action we discovered that the pyGame hitboxes for the digit buttons were not corresponding with the coordinates of the touches on the digits. Upon further investigation we discovered that when rotating the screen, the coordinates do not rotate with the screen, so the touch locations were not where we were expecting in terms of button placement. There were several scripts to recalibrate the screen and get the proper coordinate rotation matrix for the screen orientation, but we were not able to get them to run, so to stay on track in terms of timing, we manually set up ranges of coordinates for each of the buttons that would indicate a hit.

    Once we had the TFT setup in the proper orientation we were able to move on to the control script/GUI for the vending machine. The GUI constantly has a keypad display, which includes two letters ( A and B ) as well as two numbers ( 1 and 2 ), whose combinations correspond to the four prizes. There are also select and clear buttons to confirm and reverse selections respectively. There are a sequence of states that the vending machine goes through for each round of prize dispensing. Each screen has a ticket message as well as other custom messages for that state. The states after prize selection, including the buy, vend, and too few tickets screen, block the user from using the keypad until they move back into a selection state. In state 0, the screen instructs users to either scan their card to check the number of tickets that they have or select an item using the keypad. If the user scans their card, the system moves into state 1 and the number of tickets that they have on their card is displayed on the top of the screen. In this state they can scan another card to check its tickets, which will start state 1 for a second time or they can select an item. If neither of these options are performed within a specified amount of time the system will be brought back to state 0. If the user selects a valid item in either state 0 or 1, the item that they select is recorded and they are brought to state 2. The button presses are recorded in an array, with the clear button emptying the items in the array. When the select button is pressed the array is read, and a valid item is recognized if the values match the four item options. In state 2, the number of tickets the item is worth is displayed. The ticket cost of each item can be easily changed in a global array that holds the cost of each item. The GUI prompts the user to scan their card. The read and write of the RFID happens in this single scan. The card is read and the number of remaining tickets is calculated by taking the current number of tickets and subtracting the cost of the item. If this new value is below 0, the system enters state 3, otherwise the card is written with this new value and state 4 is entered to dispense the item. If the user does not scan their card within a specified time, the system returns to state 0. In state 3, the system displays a message that the user does not have enough tickets for a set amount of time and then returns to state 0. In state 4, the system runs the specified motor and then returns to state 0 after a specified amount of time, where the motors are stopped. This sequence of events allows for a non-blocking motor run.



    Vend Script State Machine

    To test the GUI, we experimented with several different sequences of user behavior to see if we got the expected outcome. We scanned the card in both states 0 and 1, as well as selected items from both of these states. We also let several of the states time out. One issue that we ran into was segfaults if we dropped the card by the reader in any of the states where the reader was actively listening for cards. We realized this occurred because the reader would recognize a card, but by the time it went to read or write the card, it was gone causing the conversion of a NoneType object. We fixed this problem through a try catch block. If at any point in the reading/writing process an error is raised, the reading/writing process will start from the beginning.

    The last component to implement to get the vending machine working was to enable motor control for the 4 motors and construct the machine itself. Due to the limited number of GPIO pins each motor shares a PWM pin and only has one GPIO input. The other input is connected to ground. This configuration is possible because each motor only needs to spin in one direction, which means that one input will always be low and the other will be low or high depending on whether it is stopped or spinning. One PWM pin used for every motor is also possible because the PWM signal will only affect the motor that is currently on. In state 4 of our system the motors run for a specified amount of time, which is the time before state 0 is entered, where the motors are shut off. Each individual motor is calibrated through the power it is at for that set amount of time. Each motor was calibrated for the speed that would dispense one item and push the next item into the first position. During calibration of each of the motors we noticed that the motors were getting stuck because the speed to slowly rotate the coils was not enough to get them to start turning. To overcome the initial force of static friction we run the motors at full speed and then lower them to the calibrated speed. The machine was constructed by twisting hangers into vending machine coils and creating compartments with bumpers for each of the coils. The coils were connected to the motors with heat shrink and hot glue. The compartments were raised off the bottom of the box to allow for a dropout area and the piTFT and RFID were mounted on the side along with the internal electronics.

Vending Machine Schematic

Internal Vending Machine Wiring


Results/Conclusions

   After much debugging and calibration we were able to get all parts of the system to function as we intended. We were able to achieve all of the goals that we originally set out to do in the project description.

   One of the large goals that we achieved was the construction of a vending machine with embedded controls. Through this process we discovered that the mechanical construction of a vending machine is more complex than we thought. Ensuring that the coils could freely turn and turn straight was a challenge especially since ours were handmade so they were not totally uniform. This non uniformness also made calibration a challenge, to turn enough to dispense only one object and not let the error build up.


Future Work

   If we had more time to work on this project the first thing we would explore is more fairly calibrating the tickets given between games. This would involve a lot of play testing to determine the difficulty and time it takes for the average player to get points in all the games. This would go along with adjusting the point costs of the prizes to match their “perceived values”.

   Another facet we’d explore if we had more time for the project is the addition of more playable games to the system. This has been made a relatively simple process thanks to our creating standardized functions for tracking game state and giving out points. All that would be required is getting the name of the rom for the game with the command print(emu.romname()) and reading the proper memory locations for the game.

   The mechanical contruction of the vending machine could also be improved. We could buy higher end motors that could handle the torque required to push several toys to the front and have a more consistent output for easier calibration. We could also get professionally made coils, which would have consitent rotation behavior.


Work Distribution

Matt Hales' Headshot

Matt Hales

mah426@cornell.edu

Decided on the OS needed for the arcade games and the right emulator to use, initially using RetroPie then settling on normal PiOS, Responsible for figuring out the memory address needed to be read for the arcade machine. Built the vending machine structure. Helped with the wiring of the arcade unit and the wiring of the motor and motor controllers for the vending machine.

Generic placeholder image

Sarah Ellenbogen

sje57@cornell.edu

Wrote the vending machine control script and set up TFT on pi Zero. Set up RFID communication. Helped with vending machine wiring and construction as well as console construction. Richard Ellenbogen helped with creation of vending machine coils.

Generic placeholder image

Zack Nelson

zen3@cornell.edu

Chose the games for their system and the specific rom versions which ran the best with the emulator. Found all the memory addresses for the arcade machine using the cheat debugger and analyzing game decompilations. Helped develop initial vend layout and plan. Developed all the subprocess controll functions including the python functions that interface with the emulator through stdin/out. Setup MAME emulator including button mapping screen rotations and seting virtual dip switches for game configuration. Created automated boot up script. Tested the overall system.


Parts List

Total: $35.09


References

RPi TFT website adafruit
Adafruit RFID tutorial
Circuit python setup tutorial
MAME Debugger
MAME Lua reference
Python Subprocess Tutorial
Autostart GUI programs

Code Appendix - GitHub

Arcade Code

import subprocess
import shlex
from time import sleep
import io
 
# rfid libs
import board
import busio
from digitalio import DigitalInOut
from adafruit_pn532.adafruit_pn532 import MIFARE_CMD_AUTH_B
from adafruit_pn532.spi import PN532_SPI
 
# SPI connection:
spi = busio.SPI(board.SCK_1, MOSI = board.MOSI_1, MISO =  board.MISO_1)
cs_pin = DigitalInOut(board.D16)
pn532 = PN532_SPI(spi, cs_pin, debug=False)
# Configure PN532 to communicate with MiFare cards
pn532.SAM_configuration()
 
NOTHING = 0
PACMAN = 1
SPACE = 2
GALA = 3
 
def display(char):
    global process
    buffer = process.stdout.read1(char).decode("utf-8")
    # print(buffer, end="", flush=True)
    return buffer
 
def search_for_output(string):
    global process
    buffer = ""
    while not (string in buffer):
        buffer = buffer + display(1)
    display(-1)
 
def runraw(command, resp):
    global process
    process.stdin.write(bytes(command + "\n", 'utf-8'))
    # print(command)
    process.stdin.flush()
    sleep(.16)
    string = display(-1)
    if resp:
        return string.split('\n')[0]
 
def run(command, resp):
    if resp:
        return int(runraw(command, resp))
    else:
        runraw(command, resp)
       
def death():
    global game
   
    run("emu.pause()", False)
    score = 0
 
    if game == PACMAN:
        temp = run("print(mem:read_u8(0xee83))", True)
        score *= 100
        score += (temp >> 4) * 10
        score += temp & 0xF
        temp = run("print(mem:read_u8(0xee82))", True)
        score *= 100
        score += (temp >> 4) * 10
        score += temp & 0xF
        sleep(0.16)
        score *= 100
        temp = run("print(mem:read_u8(0xee81))", True)
        score += (temp >> 4) * 10
        score += temp & 0xF
        sleep(0.16)
        score *= 100
        temp = run("print(mem:read_u8(0xee80))", True)
        score += (temp >> 4) * 10
        score += temp & 0xF
 
        score /= 10	# point --> ticket scaling
        score = int(score)
 
    if game == SPACE:
        temp = run("print(mem:read_u8(0x20f8))", True)
        score *= 100
        score += (temp >> 4) * 10
        score += temp & 0xF
        sleep(0.16)
        score *= 100
        temp = run("print(mem:read_u8(0x20f7))", True)
        score += (temp >> 4) * 10
        score += temp & 0xF
        sleep(0.16)
        score *= 100
        temp = run("print(mem:read_u8(0x20f6))", True)
        score += (temp >> 4) * 10
        score += temp & 0xF
 
    if game == GALA:
        score *= 10
        temp = run("print(mem:read_u8(0x83FF))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
       
        score *= 10
        temp = run("print(mem:read_u8(0x83FE))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83FD))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83FC))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83FB))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83FA))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83F9))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
        score *= 10
        temp = run("print(mem:read_u8(0x83F8))", True)
        if temp != 36:
            score += temp
        sleep(0.16)
 
    # print("score: " + str(score))
 
    key = b"\xFF\xFF\xFF\xFF\xFF\xFF"
    uid = None
    authenticated = False
    check = False
    while not check:
        try:
            while uid is None:
                # display message
                run("manager.machine:popmessage('tap card to collect your " + str(score) + " tickets')", False)
                # Check if a card is available to read
                uid = pn532.read_passive_target(timeout=2)
 
            authenticated = pn532.mifare_classic_authenticate_block(uid, 4, MIFARE_CMD_AUTH_B, key)
            if not authenticated:
                run("manager.machine:popmessage('tap card again')", False)
                uid = None
           
            # read points on card
            cardpoints = int.from_bytes(pn532.mifare_classic_read_block(4), "big")
            # write new points to card
            newpoints = score + cardpoints
            data = newpoints.to_bytes(16, 'big')
            pn532.mifare_classic_write_block(4, data)
            # confirm points from card
            check = int.from_bytes(pn532.mifare_classic_read_block(4), "big") == newpoints
 
        except:
            authenticated = False
            uid = None
            check = False
            run("Try scanning again", False)
 
    run("manager.machine:popmessage('you had " + str(cardpoints) + " tickets you now have " + str(newpoints) + " tickets!')", False)
 
    sleep(2)
    run("emu.unpause()", False)
 
alive = False
with subprocess.Popen(
    shlex.split("mame -console -nodebug -skip_gameinfo"),
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
) as process:
    sleep(1)
    display(-1)
 
    while(True):
        rom = runraw("print(emu.romname())", True)
        if rom == "pacman":
            if game != PACMAN:
                alive = False
                sleep(10)
                run("cpu = manager.machine.devices[':maincpu']", False)
                sleep(1)
                run("mem = cpu.spaces['program']", False)
                sleep(1)
                run("s = manager.machine.screens[':screen']", False)
                game = PACMAN
        elif rom == "invaderl":
            if game != SPACE:
                alive = False
                sleep(10)
                run("cpu = manager.machine.devices[':maincpu']", False)
                sleep(1)
                run("mem = cpu.spaces['program']", False)
                sleep(1)
                run("s = manager.machine.screens[':screen']", False)
                game = SPACE
        elif rom == "galagao":
            if game != GALA:
                alive = False
                sleep(10)
                run("cpu = manager.machine.devices[':maincpu']", False)
                sleep(1)
                run("mem = cpu.spaces['program']", False)
                sleep(1)
                run("s = manager.machine.screens[':screen']", False)
                game = GALA
        else:
            game = NOTHING
       
        if game == PACMAN:
            lives = run("print(mem:read_u8(0x4e14))", True)
            if lives == 3:
                alive = True
            if lives == 0 and alive:
                alive = False
                death()
 
        if game == SPACE:
            lives = run("print(mem:read_u8(0x20ef))", True)
            if lives == 1:
                alive = True
            if lives == 0 and alive:
                alive = False
                death()
       
        if game == GALA:
            lives = run("print(mem:read_u8(0x9820))", True)
            if lives == 2:
                alive = True
            if lives == 255 and alive:
                alive = False
                death()
 
        sleep(.5)

Vending Machine Code

# code for vending machine operation
import pygame     # Import pygame graphics library
from pygame.locals import *
import RPi.GPIO as GPIO
import time
import sys
import os
import sys
# import csv
import math
import board
import busio

# Additional import needed for I2C/SPI
from digitalio import DigitalInOut
from adafruit_pn532.adafruit_pn532 import MIFARE_CMD_AUTH_B

from adafruit_pn532.spi import PN532_SPI

# costs of items
cost = [2000,5000,10000000,800]

# motor config
unstall = True
spintime = .9
A1pwr = 20
A2pwr = 9
B1pwr = 7
B2pwr = 10

# SPI connection:
spi = busio.SPI(board.SCK_1, MOSI = board.MOSI_1, MISO =  board.MISO_1)
cs_pin = DigitalInOut(board.D16)
pn532 = PN532_SPI(spi, cs_pin, debug=False)

# Configure PN532 to communicate with MiFare cards
pn532.SAM_configuration()

key = b"\xFF\xFF\xFF\xFF\xFF\xFF"

# motor pins
DIRA1 = 26
DIRB1 = 13
DIRA2 = 6
DIRB2 = 5
PWM = 12

GPIO.setmode(GPIO.BCM)

# Setup motor pins
GPIO.setup(DIRA1, GPIO.OUT) # DIRA1
GPIO.setup(DIRB1, GPIO.OUT) # DIRA2
GPIO.setup(DIRA2, GPIO.OUT) # DIRB1
GPIO.setup(DIRB2, GPIO.OUT) # DIRB2
GPIO.setup(PWM,  GPIO.OUT) # PWMA
PWM = GPIO.PWM(PWM, 1000)


def A1_stop():   # Item A1 stop
    GPIO.output(DIRA1, GPIO.LOW)
    PWM.start(0)
def B1_stop():   # Item B1 stop
    GPIO.output(DIRB1, GPIO.LOW)
    PWM.start(0)
def A2_stop():   # Item A2 Stop
    GPIO.output(DIRA2, GPIO.LOW)
    PWM.start(0)
def B2_stop():   # Item B2 Stop
    GPIO.output(DIRB2, GPIO.LOW)
    PWM.start(0)
def stopAll():
    A1_stop()
    A2_stop()
    B1_stop()
    B2_stop()
def A1(pwr):   # A1 Dispense
    GPIO.output(DIRA1, GPIO.HIGH)
    PWM.start(pwr)
def B1(pwr):   # B1 Dispense
    GPIO.output(DIRB1, GPIO.HIGH)
    PWM.start(pwr)
def A2(pwr):   # A2 Dispense
    GPIO.output(DIRA2, GPIO.HIGH)
    PWM.start(pwr)
def B2(pwr):   # B2 Dispense
    GPIO.output(DIRB2, GPIO.HIGH)
    PWM.start(pwr)

os.putenv('SDL_VIDEODRIVER', 'fbcon') # Display on piTFT
os.putenv('SDL_FBDEV', '/dev/fb1') #
os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

# TFT buttons
def GPIO17_callback(channel):
    GPIO.cleanup()
    sys.exit()
GPIO.setup(17, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.add_event_detect(17, GPIO.FALLING, callback=GPIO17_callback, bouncetime=100) 


pygame.init()
pygame.mouse.set_visible(False)
size = 240,320
black = 0, 0, 0
gray = 100, 100, 100
white = 255, 255, 255
red = 255, 0, 0
screen = pygame.display.set_mode(size)
my_font = pygame.font.Font(None, 40)
message_font = pygame.font.Font(None, 30)

# Read for reference: https://pygame-zero.readthedocs.io/en/stable/ptext.html
# create buttons
my_buttons = {  'A':( 50,  220 ),   'B':( 50, 300 ),
                '1':( 120,  220 ),   '2':( 120, 300 ),
              'CLR':( 190,  220 ), 'SEL':( 190, 300 )}
rects = []
surface = []
for my_text, text_pos in my_buttons.items():
    text_surface = my_font.render(my_text, False, black)
    rect = text_surface.get_rect(center=text_pos)
    # rect.inflate(100,100)
    screen.blit(text_surface, rect)
    surface.append(text_surface)
    rects.append(rect)

# keeps track of messages
state = 0
code = ["_", "_"]
i=0
tickets = 0
message = "Scan card or pick item"
ticketStr = "No card scanned"
popUp = " "
item = 0
canSelect = 1
clock = pygame.time.Clock()
end_time = time.time() + 120
while (True):

    screen.fill(white)               # Erase the Work space
    for i in range(len(rects)):
        screen.blit(surface[i], rects[i])
    # Create textpad msg
    num_surface = my_font.render(('sel: ' + code[0] + ' ' + code[1]), True, black)
    num_rect = num_surface.get_rect(center=(60, 180))
    screen.blit(num_surface, num_rect)

    num_surface = message_font.render((ticketStr), True, black)
    num_rect = num_surface.get_rect(center=(120, 40))
    screen.blit(num_surface, num_rect)

    num_surface = message_font.render((message), True, black)
    num_rect = num_surface.get_rect(center=(120, 100))
    screen.blit(num_surface, num_rect)

    num_surface = my_font.render((popUp), True, red)
    num_rect = num_surface.get_rect(center=(120, 150))
    screen.blit(num_surface, num_rect)

    if state == 0:
        message = "Scan card or pick item"
        ticketStr = "No card scanned"
        popUp = " "
        stopAll()
        item = 0
        canSelect = 1
        uid = None
        #RFID read here if read tickets == read
        # Check if a card is available to read
        uid = pn532.read_passive_target(timeout=1)
        if uid is not None:
            try:
                authenticated = pn532.mifare_classic_authenticate_block(uid, 4, MIFARE_CMD_AUTH_B, key)
                tickets = int.from_bytes(pn532.mifare_classic_read_block(4), "big")
                state = 1
                state1start = time.time()
            except:
                uid = None
                print("except")

    elif state == 1:
        popUp = " "
        ticketStr = "You have " + str(tickets) + " tickets"
        canSelect = 1
        if time.time() > (state1start + 5):
            uid = pn532.read_passive_target(timeout=1)
            if uid is not None:
                try:
                    authenticated = pn532.mifare_classic_authenticate_block(uid, 4, MIFARE_CMD_AUTH_B, key)
                    tickets = int.from_bytes(pn532.mifare_classic_read_block(4), "big")
                    state = 1
                    state1start = time.time()
                except:
                    uid = None
                    print("exception")
        if (time.time() >= state1start + 20):
            state = 0

    elif state == 2:
        popUp = "SCAN CARD"
        canSelect = 0
        if (time.time() >= state2start + 5):
            state = 0
        uid = pn532.read_passive_target(timeout=1)
        if uid is not None:
            try:
                authenticated = pn532.mifare_classic_authenticate_block(uid, 4, MIFARE_CMD_AUTH_B, key)
                vend_tickets = int.from_bytes(pn532.mifare_classic_read_block(4), "big")
                new_val = vend_tickets - cost[item-1]
                if new_val >= 0:
                    data = new_val.to_bytes(16, 'big')
                    pn532.mifare_classic_write_block(4, data)
                    ticketStr = "You now have " + str(new_val) + " tickets"
                    state = 4
                    state4start = time.time() 
                else:
                    state = 3
                    state3start = time.time()
            except:
                uid = None
                print("exception")

    elif state == 3:
        popUp = " "
        canSelect = 0
        message = "Too few tickets"
        if (time.time() >= state3start + 3):
            state = 0

    elif state == 4:
        popUp = "VENDING"
        canSelect = 0
        if (time.time() >= state4start + spintime):
            state = 0
        if item == 1:
            if unstall:
                A1(100)
                time.sleep(.05)
            A1(A1pwr)
        elif item == 2:
            if unstall:
                A2(70)
                time.sleep(.05)
            A2(A2pwr)
        elif item == 3:
            if unstall:
                B1(100)
                time.sleep(.05)
            B1(B1pwr)
        elif item == 4:
            if unstall:
                B2(70)
                time.sleep(.05)
            B2(B2pwr)

    if canSelect == 1:

        for event in pygame.event.get():
            if(event.type is MOUSEBUTTONUP):
                x,y = pygame.mouse.get_pos()
                pos = pygame.mouse.get_pos()
                print(pos)

                if x >= 175 and x <= 205 and y >= 80 and y <= 115:
                    print("A")
                    print(code[0] + code[1])
                    if (code[0] == "_") :
                        code[0] = "A"
                    elif (code[1] == "_") :
                        code[1] = "A"

                elif x >= 175 and x <= 205 and y >= 0 and y <= 30:
                    print("B")
                    print(code[0] + code[1])
                    if (code[0] == "_") :
                        code[0] = 'B'
                    elif (code[1] == "_") :
                        code[1] = 'B'

                elif x >= 105 and x <= 135 and y >= 80 and y <= 115:
                    print("1")
                    print(code[0] + code[1])
                    if (code[0] == "_") :
                        code[0] = "1"
                    elif (code[1] == "_") :
                        code[1] = "1"

                elif x >= 105 and x <= 135 and y >= 0 and y <= 30:
                    print("2")
                    print(code[0] + code[1])
                    if (code[0] == "_") :
                        code[0] = "2"
                    elif (code[1] == "_") :
                        code[1] = "2"

                elif x >= 10 and x <= 75 and y >= 80 and y <= 115:
                    code = ["_", "_"]
                    print("CLR")

                elif x >= 10 and x <= 75 and y >= 0 and y <= 30:
                    if code[0] == "A":
                        if code[1] == "1":
                            message = "This prize is " + str(cost[0]) + " tickets"
                            state = 2
                            item = 1
                        if code[1] == "2":
                            message = "This prize is " + str(cost[1]) + " tickets"
                            state = 2
                            item = 2

                    if code[0] == "B":
                        if code[1] == "1":
                            message = "This prize is " + str(cost[2]) + " tickets"
                            state = 2
                            item = 3
                        if code[1] == "2":
                            message = "This prize is " + str(cost[3]) + " tickets"
                            state = 2
                            item = 4

                    if state != 2:
                        state = 0
                    else:
                        state2start = time.time()

                    code = ["_", "_"]
                    print("SEL")
    
    # Render
    screen.blit(pygame.transform.rotate(screen, 180), (0, 0))
    clock.tick(30)
    pygame.display.flip()

# Close program
print("time out")
GPIO.cleanup() 
sys.exit()